﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Implements a music section interior for the common western music notation.
    /// </summary>
    public class MusicSectionInterior : SectionInteriorElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="MusicSectionInterior"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="MusicSectionElement"/>
        /// </para>
        /// <para>
        /// The music section that directly owns this interior.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        private MusicSectionInterior(XmlPackagePart packagePart, MusicSectionElement owner)
            : base(packagePart, owner)
        {
            systemsCollection = new ElementCollection<System>(this, systemsElement);
            systems = new ReadOnlyCollection<System>(systemsCollection);

            FindOwner<MusicSectionElement>().PropertyChanged += new PropertyChangedEventHandler(MusicSection_PropertyChanged);
        }

        /// <summary>
        /// Creates a new <see cref="MusicSectionInterior"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="MusicSectionElement"/>
        /// </para>
        /// <para>
        /// The music section that directly owns this interior.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal MusicSectionInterior(XmlPackagePart packagePart, MusicSectionElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            systemsElement = element[PackageVocabulary.SystemElementName + "s", PackageVocabulary.PackageNamespaceUri];

            systemsCollection = new ElementCollection<System>(this, systemsElement);
            systems = new ReadOnlyCollection<System>(systemsCollection);

            FindOwner<MusicSectionElement>().PropertyChanged += new PropertyChangedEventHandler(MusicSection_PropertyChanged);
        }

        #endregion

        #region SectionInteriorElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElementCore(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.CWSectionInteriorElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            systemsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.SystemElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            element.AppendChild(systemsElement);

            return element;
        }

        /// <inheritdoc/>
        protected override void CreateContentCore(object parameter)
        {
            var section = FindOwner<MusicSectionElement>();
            if (section.FirstPresentedMeasure == null || section.LastPresentedMeasure == null)
                return;

            var composition = section.Composition.Composition;

            if (FindOwner<PageElement>() == null)   // Linear View
            {
                var system = new System(PackagePart, this, section.FirstPresentedMeasure, section.LastPresentedMeasure);
                systemsCollection.Add(system);

                var stavesHeight = system.Staves.Sum(s => s.Height);
                section.Height = 2.5 * stavesHeight;
                Layout();
            }
            else
            {
                int measuresPerSystem;
                var measuresCount = section.LastPresentedMeasure.Number - section.FirstPresentedMeasure.Number + 1;
                if (parameter == null || !int.TryParse(parameter.ToString(), out measuresPerSystem))
                    measuresPerSystem = (measuresCount) / ComputeSystemsCount(measuresCount);
                
                if (measuresPerSystem < 1)
                    measuresPerSystem = 1;                
                CreateSystems(measuresPerSystem);

                Layout();
            }
        }

        /// <inheritdoc/>
        internal override bool HandleBorderRequest(Internal.SectionBorderRequest request)
        {
            if (request.Request == Internal.BorderRequest.PROCESS_INNER_REQUEST)
            {
                var rq = (Internal.SystemBorderRequest)request.InnerRequest;
                var step = (request.RequestDirection == Internal.Direction.Left ? -1 : 1);
                var index = systems.IndexOf(rq.Sender);

                for (int i = index + step; i >= 0 && i < systems.Count; i += step)
                {
                    if (systems[i].HandleBorderRequest(rq))
                        return true;
                }
            }

            return false;
        }

        /// <inheritdoc/>
        internal override void ProcessBorderRequest(Internal.InteriorRootBorderRequest request)
        {
            if (request == null)
                throw new InvalidOperationException("The request cannot be null!");
            if (!(request is Internal.SystemBorderRequest))
                throw new InvalidOperationException("Unrecognized request!");

            var rq = (Internal.SystemBorderRequest)request;
            var step = (request.RequestDirection == Internal.Direction.Left ? -1 : 1);
            var index = systems.IndexOf(rq.Sender);
            if (index == -1)    // The request comes from a system that was not yet added to the systems collection
            {
                if (request.RequestDirection == Internal.Direction.Left)
                    index = systems.Count;
            }

            bool handled = false;

            for (int i = index + step; i >= 0 && i < systems.Count; i += step)
            {
                if (systems[i].HandleBorderRequest(rq))
                {
                    handled = true;
                    break;
                }
            }

            if (!handled && FindOwner<PageElement>() != null)
            {
                var ms = FindOwner<MusicSectionElement>();
                var measure = (ushort)(request.RequestDirection == Internal.Direction.Left ? ms.FirstPresentedMeasure.Number - 1 : ms.LastPresentedMeasure.Number + 1);
                var mrq = new Internal.MusicSectionBorderRequest(
                    Internal.BorderRequest.PROCESS_INNER_REQUEST,
                    request.RequestDirection,
                    ms
                    );
                mrq.Measure = measure;
                mrq.PresentedComposition = ms.Composition;
                mrq.InteriorType = GetType();
                mrq.InnerRequest = request;

                ms.FindOwner<PageElement>().ProcessBorderRequest(mrq);
            }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the value of some property of the owner <see cref="MusicSectionElement"/>
        /// has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If the modified property is <see cref="MusicSectionElement.FirstPresentedMeasure"/>
        /// or <see cref="MusicSectionElement.LastPresentedMeasure"/> the handler modifies
        /// the measure presented by its systems.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void MusicSection_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "FirstPresentedMeasure")
            {
                var section = FindOwner<MusicSectionElement>();

                if (section.FirstPresentedMeasure == null || section.LastPresentedMeasure == null)
                {
                    systemsCollection.Clear();
                    return;
                }

                if (systemsCollection.Count == 0)
                {
                    var count = section.LastPresentedMeasure.Number - section.FirstPresentedMeasure.Number + 1;
                    var systemsCount = ComputeSystemsCount(count);

                    CreateSystems((int)Math.Ceiling((decimal)count / systemsCount));
                }
                else
                    systemsCollection[0].SetMeasures(section.FirstPresentedMeasure, systemsCollection[0].Measures[systemsCollection[0].Measures.Count - 1]);

                if (FindOwner<PageElement>() != null)
                    Layout();
            }
            else if (e.PropertyName == "LastPresentedMeasure")
            {
                var section = FindOwner<MusicSectionElement>();

                if (section.FirstPresentedMeasure == null || section.LastPresentedMeasure == null)
                {
                    systemsCollection.Clear();
                    return;
                }

                if (systemsCollection.Count == 0)
                {
                    var count = section.LastPresentedMeasure.Number - section.FirstPresentedMeasure.Number + 1;
                    var systemsCount = ComputeSystemsCount(count);

                    CreateSystems((int)Math.Ceiling((decimal)count / systemsCount));
                }
                else
                    systemsCollection[systemsCollection.Count - 1].SetMeasures(systemsCollection[systemsCollection.Count - 1].Measures[0], section.LastPresentedMeasure);

                if (FindOwner<PageElement>() != null)
                    Layout();
            }
            else if ((e.PropertyName == "Width" || e.PropertyName == "Height") && FindOwner<PageElement>() != null)
            {
                Layout();

                foreach (var system in systems)
                    CWNotation.Layout.Layouter.LayoutHorizontally(system);
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Computes the count of the systems necessary to hold a given number of measures.
        /// </summary>
        /// <remarks>
        /// The method uses a simple algorithm:
        /// <list type="number">
        /// <item>
        ///     If the section is in a linear view mode the method returns 1.
        /// </item>
        /// <item>
        ///     Otherwise, an approximate system height is computed using a following formula:<br/><br/>
        ///     <i>Height of the owner <see cref="MusicSectionElement"/> / (Sum of staves height * 2.2)</i><br/><br/>
        ///     The 2.2 factor was added to simulate the spaces between staves and systems.
        /// </item>
        /// <item>
        ///     If the number of measures per system for the computed systems count is less than 3
        ///     the systems count is lowered to the ceiling of <paramref name="measuresCount"/> / 3.
        /// </item>
        /// </list>
        /// </remarks>
        /// <param name="measuresCount">The total number of measures</param>
        /// <returns>The number of systems necessary to hold the measures</returns>
        private int ComputeSystemsCount(int measuresCount)
        {
            if (FindOwner<PageElement>() == null)   // The section is not inside a page => Linear View
                return 1;

            var stavesHeight = (from part in FindOwner<MusicSectionElement>().Parts
                                from staff in part.FindCustomData<CWPartData>().Staves
                                select staff.DefaultHeight).Sum();
            var systemHeight = stavesHeight * 2.2;
            int systemsCount = (int)(FindOwner<MusicSectionElement>().Height / systemHeight);
            if (systemsCount < 1)
                systemsCount = 1;
            if (measuresCount / systemsCount < 3)
                systemsCount = measuresCount / 3 + (measuresCount % 3 > 0 ? 1 : 0);

            return systemsCount;
        }

        /// <summary>
        /// Creates the systems each holding a given number of measures.
        /// </summary>
        /// <param name="measuresPerSystem">The number of systems per one measure</param>
        private void CreateSystems(int measuresPerSystem)
        {
            var owner = FindOwner<MusicSectionElement>();
            if (owner.FirstPresentedMeasure == null || owner.LastPresentedMeasure == null)
            {
                systemsCollection.Add(new System(PackagePart, this, null, null));
                return;
            }
            var composition = owner.Composition.Composition;
            var measuresCount = owner.LastPresentedMeasure.Number - owner.FirstPresentedMeasure.Number + 1;
            int systemsCount = measuresCount / measuresPerSystem;
            ushort actualMeasure = owner.FirstPresentedMeasure.Number;

            for (int i = 0; i < systemsCount; ++i)
            {
                var system = new System(PackagePart, this, composition[actualMeasure], composition[(ushort)(actualMeasure + measuresPerSystem - 1)]);
                systemsCollection.Add(system);
                actualMeasure += (ushort)measuresPerSystem;
            }

            if (measuresCount % measuresPerSystem > 0)
            {
                var system = new System(PackagePart, this, composition[actualMeasure], composition[(ushort)(actualMeasure + (measuresCount % measuresPerSystem) - 1)]);
                systemsCollection.Add(system);
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new <see cref="MusicSectionInterior"/> instance.
        /// </summary>
        /// <remarks>
        /// This method can be passed as <see cref="SectionInteriorCreator"/>.
        /// </remarks>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns the element.
        /// </para>
        /// </param>
        /// <param name="section">
        /// <para>
        /// Type: <see cref="SectionElement"/>
        /// </para>
        /// <para>
        /// The section that directly owns the element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="SectionInteriorElement"/>
        /// </para>
        /// <para>
        /// The created section interior element.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="section"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="section"/> is not a <see cref="MusicSectionElement"/>
        /// </exception>
        public static SectionInteriorElement Create(XmlPackagePart packagePart, SectionElement section)
        {
            if (!(section is MusicSectionElement))
                throw new ArgumentException("This interior can only be created inside a MusicSectionElement!", "owner");

            return new MusicSectionInterior(packagePart, section as MusicSectionElement);
        }

        /// <summary>
        /// Layouts the musical content.
        /// </summary>
        public void Layout()
        {
            double basicStavesDistance = 1.0;       // Basic distance between two staves of the same part (in free space units)
            double basicPartsDistance = 1.1;        // Basic distance between two staves of different parts (in free space units)
            double basicSystemsDistance = 1.3;      // Basic distance between two systems (in free space units)

            double freeSpaceUnits = 0.0;            // Total number of free space units needed
            double occupiedVSpace = 0.0;            // Total occupied space in the vertical direction (in centimeters)

            foreach (System system in systemsCollection)
            {
                if (system.Staves.Count > 0)
                {
                    var lastPart = system.Staves[0].Descriptor.FindOwner<PartDescriptorElement>();

                    foreach (Staff staff in system.Staves)
                    {
                        occupiedVSpace += staff.Height;

                        if (staff.Descriptor.FindOwner<PartDescriptorElement>() == lastPart)
                            freeSpaceUnits += basicStavesDistance;
                        else
                        {
                            freeSpaceUnits += basicPartsDistance;
                            lastPart = staff.Descriptor.FindOwner<PartDescriptorElement>();
                        }
                    }

                    freeSpaceUnits += basicSystemsDistance;
                }
            }

            var section = FindOwner<MusicSectionElement>();
            double totalVSpace = section.Height - section.Margin.Top - section.Margin.Bottom - occupiedVSpace;
            double totalHSpace = section.Width - section.Margin.Left - section.Margin.Right;
            double freeSpaceUnit = totalVSpace / freeSpaceUnits;

            double actualTop = section.Margin.Top + basicPartsDistance * freeSpaceUnit;

            foreach (System system in systemsCollection)
            {
                system.Top = actualTop;
                system.Left = section.Margin.Left + 0.3;
                system.Width = totalHSpace - 0.3;

                if (system.Staves.Count > 0)
                {
                    var lastPart = system.Staves[0].Descriptor.FindOwner<PartDescriptorElement>();
                    double actualStaffTop = 0.0;

                    foreach (Staff staff in system.Staves)
                    {
                        staff.Top = actualStaffTop;
                        actualStaffTop += staff.Height;

                        if (staff.Descriptor.FindOwner<PartDescriptorElement>() == lastPart)
                            actualStaffTop += basicStavesDistance * freeSpaceUnit;
                        else
                        {
                            actualStaffTop += basicPartsDistance * freeSpaceUnit;
                            lastPart = staff.Descriptor.FindOwner<PartDescriptorElement>();
                        }
                    }

                    system.Height = actualStaffTop;
                    actualTop += actualStaffTop + basicSystemsDistance * freeSpaceUnit;
                }
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of systems.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="System"/>
        /// </para>
        /// <para>
        /// The collection of systems.
        /// </para>
        /// </value>
        public ReadOnlyCollection<System> Systems
        {
            get { return systems; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Systems"/> property.
        /// </summary>
        private ReadOnlyCollection<System> systems;
        /// <summary>
        /// Holds the collection of systems.
        /// </summary>
        private ElementCollection<System> systemsCollection;
        /// <summary>
        /// Holds the XML element that holds the <see cref="systemsCollection"/>.
        /// </summary>
        private XmlElement systemsElement;

        #endregion
    }
}
